﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media.Imaging;
using System.Xml;
using System.Xml.Linq;
using ICSharpCode.SharpZipLib.Zip;

namespace FB2er
{
    public class FB2Loader
    {
        private const string LineIndent = "      ";
        private string[] _partIDs;
        private FlowDocument _doc;
        private Dictionary<string, Collection<Image>> _images;
        private Dictionary<string, Paragraph> _notes;
        private XmlReader _rdr;
        private Paragraph _currP;
        private bool _advanced;
        private bool _isBold;
        private bool _isItalic;
        private bool _isStrikethrough;
        private bool _isTitle;
        private bool _isEpigraph;
        private bool _isPoem;
        private bool _isSubtitle;
        private bool _lineBreakMode;

        public void LoadFB2(string fileName, FlowDocument doc)
        {
            _doc = doc;
            _doc.Blocks.Clear();
            using (var s = new ZipInputStream(File.OpenRead(fileName)))
            {
                s.GetNextEntry();
                _rdr = XmlReader.Create(s, new XmlReaderSettings { IgnoreWhitespace = true });
                using (_rdr)
                {
                    const int ciMaxDepth = 10;
                    _partIDs = new string[ciMaxDepth];
                    _images = new Dictionary<string, Collection<Image>>();
                    _notes = new Dictionary<string, Paragraph>();
                    _rdr.ReadToFollowing("FictionBook");
                    while (_advanced || _rdr.Read())
                    {
                        _advanced = false;
                        if (_rdr.Depth > ciMaxDepth) continue; // in case of errors in the document, just skip over
                        switch (_rdr.NodeType)
                        {
                            case XmlNodeType.Element:
                                _partIDs[_rdr.Depth - 1] = _rdr.Name;
                                switch (_rdr.Name)
                                {
                                    case "description":
                                        ProcessDescription();
                                        break;
                                    case "p":
                                        if (_lineBreakMode)
                                        {
                                            CurrentParagraph.Inlines.Add(new LineBreak());
                                            CurrentParagraph.Inlines.Add(LineIndent);
                                        }
                                        else
                                        {
                                            ResetParagraph();
                                            _lineBreakMode = true;
                                        }
                                        break;
                                    case "image":
                                        ProcessImage();
                                        break;
                                    case "binary":
                                        ProcessBinary();
                                        break;
                                    case "a":
                                        ProcessAnchor();
                                        break;
                                    case "emphasis":
                                        _isItalic = true;
                                        break;
                                    case "cite":
                                        _isItalic = true;
                                        break;
                                    case "strong":
                                        _isBold = true;
                                        break;
                                    case "strikethrough":
                                        _isStrikethrough = true;
                                        break;
                                    case "title":
                                        if (!_isTitle)
                                        {
                                            _isTitle = true;
                                            ResetParagraph();
                                        }
                                        break;
                                    case "subtitle":
                                        if (!_isSubtitle)
                                        {
                                            _isSubtitle = true;
                                            ResetParagraph();
                                        }
                                        break;
                                    case "epigraph":
                                        if (!_isEpigraph)
                                        {
                                            _isEpigraph = true;
                                            ResetParagraph();
                                        }
                                        break;
                                    case "text-author":
                                        _isBold = true;
                                        _isItalic = true;
                                        CurrentParagraph.Inlines.Add(new LineBreak());
                                        break;
                                    case "poem":
                                        _isPoem = true;
                                        ResetParagraph();
                                        break;
                                    case "stanza":
                                        ResetParagraph();
                                        break;
                                    case "v":
                                        CurrentParagraph.Inlines.Add(new LineBreak());
                                        CurrentParagraph.Inlines.Add(LineIndent);
                                        CurrentParagraph.Inlines.Add(LineIndent);
                                        break;
                                    case "body":
                                        if (_rdr["name"] == "notes")
                                            ProcessNotes();
                                        break;
                                    case "empty-line":
                                        ResetParagraph();
                                        break;
                                    case "section":
                                        break; // do-nothing
                                    default:
                                        Debug.WriteLine(@"Unknown element: {0}", _rdr.Name);
                                        break;
                                }
                                break;
                            case XmlNodeType.EndElement:
                                switch (_rdr.Name)
                                {
                                    case "emphasis":
                                        _isItalic = false;
                                        break;
                                    case "cite":
                                        _isItalic = false;
                                        break;
                                    case "strong":
                                        _isBold = false;
                                        break;
                                    case "strikethrough":
                                        _isStrikethrough = false;
                                        break;
                                    case "title":
                                        _isTitle = false;
                                        _lineBreakMode = false;
                                        break;
                                    case "subtitle":
                                        _isSubtitle = false;
                                        _lineBreakMode = false;
                                        break;
                                    case "epigraph":
                                        _isEpigraph = false;
                                        _lineBreakMode = false;
                                        break;
                                    case "poem":
                                        _isPoem = false;
                                        _lineBreakMode = false;
                                        break;
                                    case "text-author":
                                        _isBold = false;
                                        _isItalic = false;
                                        break;
                                }
                                break;
                            case XmlNodeType.Text:
                                ProcessText();
                                break;
                        }
                    }
                }
            }
        }

        private void ResetParagraph()
        {
            _currP = null;
        }

        private Paragraph CurrentParagraph
        {
            get
            {
                if (_currP == null)
                {
                    _currP = new Paragraph();
                    CurrentParagraph.Inlines.Add(LineIndent);
                    _doc.Blocks.Add(_currP);
                }
                return _currP;
            }
        }

        private string Href()
        {
            var res = _rdr["l:href"];
            if (string.IsNullOrEmpty(res))
                res = _rdr["xlink:href"];
            return res;
        }

        private void AddImage(string key, Image img)
        {
            if (!_images.ContainsKey(key))
                _images[key] = new Collection<Image> { img };
            else
                _images[key].Add(img);
        }

        private void ProcessImage()
        {
            var href = Href();
            if (string.IsNullOrEmpty(href)) return;
            var img = new Image();
            AddImage(href.Substring(1), img);
            ResetParagraph();
            _doc.Blocks.Add(new BlockUIContainer(img));
        }

        private void ProcessCoverPage(XElement coverpage)
        {
            if (coverpage == null) return;
            var imgAttr = coverpage.FirstAttribute;
            if (imgAttr == null) return;
            var imgRef = imgAttr.Value.Substring(1);
            if (string.IsNullOrEmpty(imgRef)) return;
            var img = new Image();
            AddImage(imgRef, img);
            _doc.Blocks.Add(new BlockUIContainer(img));
        }

        private void ProcessDescription()
        {
            var desc = (XElement)XNode.ReadFrom(_rdr);
            _advanced = true;
            var descNS = desc.Name.Namespace;
            XName[] coverNames = { descNS + "title-info", descNS + "coverpage", descNS + "image" };
            ProcessCoverPage(desc.ChildElement(coverNames));
        }

        private void ProcessNotes()
        {
            var notes = (XElement)XNode.ReadFrom(_rdr);
            _advanced = true;
            var notesNS = notes.Name.Namespace;
            XName[] titleNames = { notesNS + "title", notesNS + "p" };
            foreach (var note in notes.Descendants(notesNS + "section"))
            {
                var id = note.AttributeString("id");
                var sp = new Span { BaselineAlignment = BaselineAlignment.Superscript };
                sp.Inlines.Add(note.ElementsString(titleNames) + " ");
                _notes[id].Inlines.Add(sp);
                _notes[id].Inlines.Add(note.ElementString(notesNS + "p"));
            }
        }

        private void ProcessAnchor()
        {
            var anchor = Href();
            if (string.IsNullOrEmpty(anchor)) return;
            var p = new Paragraph();
            TextElement.SetFontSize(p, 10);
            if (anchor.Contains(":"))
            {
                Uri uri;
                try
                {
                    uri = new Uri(anchor);
                }
                catch (Exception)
                {
                    uri = null;
                }
                var hl = new Hyperlink(new Run(_rdr.ReadElementContentAsString()))
                             {
                                 NavigateUri = uri,
                             };
                hl.Click += HlClick;
                CurrentParagraph.Inlines.Add(hl);
            }
            else
            {
                _notes[anchor.Substring(1)] = p;
                var sp = new Span { BaselineAlignment = BaselineAlignment.Superscript };
                sp.Inlines.Add(_rdr.ReadElementContentAsString());
                CurrentParagraph.Inlines.Add(sp);
                var figure = new Figure(p)
                                 {
                                     Width = new FigureLength(1, FigureUnitType.Content),
                                     VerticalAnchor = FigureVerticalAnchor.PageBottom,
                                     HorizontalAnchor = FigureHorizontalAnchor.ColumnCenter,
                                     BorderThickness = new Thickness(0, 1, 0, 0),
                                     BorderBrush = SystemColors.ActiveBorderBrush,
                                 };
                CurrentParagraph.Inlines.Add(figure);
            }
            ProcessText();
        }

        static void HlClick(object sender, RoutedEventArgs e)
        {
            if (((Hyperlink)sender).NavigateUri != null)
                Process.Start(((Hyperlink)sender).NavigateUri.AbsoluteUri);
        }

        private void ProcessBinary()
        {
            var imgID = _rdr["id"];
            var imgType = _rdr["content-type"];
            _rdr.Read();
            var imgString = _rdr.Value;
            if (!_images.ContainsKey(imgID)) return;
            var ms = new MemoryStream(Convert.FromBase64String(imgString));
            BitmapFrame imgSrc;
            switch (imgType)
            {
                case "image/jpeg":
                    imgSrc = new JpegBitmapDecoder(ms, BitmapCreateOptions.None, BitmapCacheOption.None).Frames[0];
                    break;
                case "image/jpg":
                    imgSrc = new JpegBitmapDecoder(ms, BitmapCreateOptions.None, BitmapCacheOption.None).Frames[0];
                    break;
                case "image/png":
                    imgSrc = new PngBitmapDecoder(ms, BitmapCreateOptions.None, BitmapCacheOption.None).Frames[0];
                    break;
                default:
                    Debug.WriteLine("Unknown image type : {0}", imgType);
                    return;
            }
            foreach (var img in _images[imgID])
            {
                img.MaxHeight = imgSrc.PixelHeight;
                img.MaxWidth = imgSrc.PixelWidth;
                img.Source = imgSrc;
            }
        }

        private void ProcessText()
        {
            var run = new Run(_rdr.Value);
            CurrentParagraph.Inlines.Add(run);
            if (_isTitle)
            {
                CurrentParagraph.KeepWithNext = true;
                TextElement.SetFontSize(run, 20);
                TextElement.SetFontWeight(run, FontWeights.Bold);
            }
            if (_isSubtitle)
            {
                CurrentParagraph.KeepWithNext = true;
                TextElement.SetFontSize(run, 16);
                TextElement.SetFontWeight(run, FontWeights.Bold);
            }
            if (_isEpigraph)
            {
                CurrentParagraph.KeepWithNext = true;
                TextElement.SetFontStyle(run, FontStyles.Italic);
                CurrentParagraph.TextAlignment = TextAlignment.Right;
            }
            if (_isPoem)
            {
                CurrentParagraph.KeepWithNext = true;
                TextElement.SetFontStyle(run, FontStyles.Italic);
            }
            if (_isItalic)
                TextElement.SetFontStyle(run, FontStyles.Italic);
            if (_isBold)
                TextElement.SetFontWeight(run, FontWeights.Bold);
            if (_isStrikethrough)
            {
                run.TextDecorations.Add(new TextDecoration { Location = TextDecorationLocation.Strikethrough });
            }
        }
    }

    public static class XElementHelper
    {
        public static string ElementString(this XElement el, XName elementName)
        {
            var child = el.Element(elementName);
            return child == null ? "" : child.Value;
        }

        public static string ElementsString(this XElement el, IEnumerable<XName> elementNames)
        {
            var child = el;
            foreach (var name in elementNames)
            {
                if (child == null)
                    return "";
                child = child.Element(name);
            }
            return child == null ? "" : child.Value;
        }

        public static XElement ChildElement(this XElement el, IEnumerable<XName> elementNames)
        {
            var child = el;
            foreach (var name in elementNames)
            {
                if (child == null)
                    return child;
                child = child.Element(name);
            }
            return child;
        }

        public static string AttributeString(this XElement el, XName attributeName)
        {
            var child = el.Attribute(attributeName);
            return child == null ? "" : child.Value;
        }
    }
}
